import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:stream_transform/stream_transform.dart';

import 'package:thunder/src/core/enums/comment_sort_type.dart';
import 'package:thunder/l10n/generated/app_localizations.dart';
import 'package:thunder/src/core/models/thunder_private_message.dart';
import 'package:thunder/src/features/account/account.dart';
import 'package:thunder/src/features/comment/comment.dart';
import 'package:thunder/src/features/inbox/inbox.dart';
import 'package:thunder/src/features/notification/notification.dart';
import 'package:thunder/src/app/utils/global_context.dart';

part 'inbox_event.dart';
part 'inbox_state.dart';

const throttleDuration = Duration(seconds: 1);
const timeout = Duration(seconds: 5);

EventTransformer<E> throttleDroppable<E>(Duration duration) {
  return (events, mapper) => droppable<E>().call(events.throttle(duration), mapper);
}

class InboxBloc extends Bloc<InboxEvent, InboxState> {
  Account account;

  late CommentRepository commentRepository;
  late NotificationRepository notificationRepository;

  /// Constructor allowing an initial set of replies to be set in the state.
  InboxBloc.initWith({
    required List<ThunderComment> replies,
    required bool showUnreadOnly,
    required this.account,
  }) : super(InboxState(replies: replies, showUnreadOnly: showUnreadOnly)) {
    commentRepository = CommentRepositoryImpl(account: account);
    notificationRepository = NotificationRepositoryImpl(account: account);
    _init();
  }

  /// Unnamed constructor with default state
  InboxBloc({required this.account}) : super(const InboxState()) {
    commentRepository = CommentRepositoryImpl(account: account);
    notificationRepository = NotificationRepositoryImpl(account: account);
    _init();
  }

  void _init() {
    on<GetInboxEvent>(_getInboxEvent, transformer: restartable());
    on<InboxItemActionEvent>(_inboxItemActionEvent);
    on<MarkAllAsReadEvent>(_markAllAsRead);
  }

  Future<void> _getInboxEvent(GetInboxEvent event, emit) async {
    if (account.anonymous) {
      return emit(state.copyWith(
        status: InboxStatus.empty,
        privateMessages: [],
        mentions: [],
        replies: [],
        showUnreadOnly: !event.showAll,
        inboxMentionPage: 1,
        inboxReplyPage: 1,
        inboxPrivateMessagePage: 1,
        totalUnreadCount: 0,
        repliesUnreadCount: 0,
        mentionsUnreadCount: 0,
        messagesUnreadCount: 0,
        hasReachedInboxReplyEnd: true,
        hasReachedInboxMentionEnd: true,
        hasReachedInboxPrivateMessageEnd: true,
      ));
    }

    int limit = 20;

    try {
      List<ThunderPrivateMessage>? privateMessagesResponse = [];
      List<ThunderComment> mentionsResponse = [];
      List<ThunderComment> repliesResponse = [];

      if (event.reset) {
        emit(state.copyWith(status: InboxStatus.loading, errorMessage: ''));

        switch (event.inboxType) {
          case InboxType.replies:
            repliesResponse = await notificationRepository.replies(
              unread: !event.showAll,
              limit: limit,
              sort: event.commentSortType,
              page: 1,
            );
            break;
          case InboxType.mentions:
            mentionsResponse = await notificationRepository.mentions(
              unread: !event.showAll,
              limit: limit,
              sort: event.commentSortType,
              page: 1,
            );
            break;
          case InboxType.messages:
            privateMessagesResponse = await notificationRepository.messages(
              unread: !event.showAll,
              limit: limit,
              page: 1,
            );
            break;
          case InboxType.all:
            repliesResponse = await notificationRepository.replies(
              unread: !event.showAll,
              limit: limit,
              sort: event.commentSortType,
              page: 1,
            );

            mentionsResponse = await notificationRepository.mentions(
              unread: !event.showAll,
              limit: limit,
              sort: event.commentSortType,
              page: 1,
            );

            privateMessagesResponse = await notificationRepository.messages(
              unread: !event.showAll,
              limit: limit,
              page: 1,
            );
            break;
          default:
            break;
        }

        final unread = await notificationRepository.unreadNotificationsCount();
        int totalUnreadCount = unread['private_messages'] + unread['mentions'] + unread['replies'];

        return emit(
          state.copyWith(
            status: InboxStatus.success,
            privateMessages: cleanDeletedMessages(privateMessagesResponse),
            mentions: cleanDeletedMentions(mentionsResponse),
            replies: repliesResponse,
            showUnreadOnly: !event.showAll,
            inboxMentionPage: 2,
            inboxReplyPage: 2,
            inboxPrivateMessagePage: 2,
            totalUnreadCount: totalUnreadCount,
            repliesUnreadCount: unread['replies'],
            mentionsUnreadCount: unread['mentions'],
            messagesUnreadCount: unread['private_messages'],
            hasReachedInboxReplyEnd: repliesResponse.isEmpty || repliesResponse.length < limit,
            hasReachedInboxMentionEnd: mentionsResponse.isEmpty == true || mentionsResponse.length < limit,
            hasReachedInboxPrivateMessageEnd: privateMessagesResponse.isEmpty == true || privateMessagesResponse.length < limit,
          ),
        );
      }

      // Prevent fetching if we're already fetching
      if (state.status == InboxStatus.refreshing) return;
      emit(state.copyWith(status: InboxStatus.refreshing, errorMessage: ''));

      switch (event.inboxType) {
        case InboxType.replies:
          if (state.hasReachedInboxReplyEnd) return;
          repliesResponse = await notificationRepository.replies(
            unread: state.showUnreadOnly,
            limit: limit,
            sort: event.commentSortType,
            page: state.inboxReplyPage,
          );
          break;
        case InboxType.mentions:
          if (state.hasReachedInboxMentionEnd) return;
          mentionsResponse = await notificationRepository.mentions(
            unread: state.showUnreadOnly,
            limit: limit,
            sort: event.commentSortType,
            page: state.inboxMentionPage,
          );
          break;
        case InboxType.messages:
          if (state.hasReachedInboxPrivateMessageEnd) return;
          privateMessagesResponse = await notificationRepository.messages(
            unread: state.showUnreadOnly,
            limit: limit,
            page: state.inboxPrivateMessagePage,
          );
          break;
        default:
          break;
      }

      List<ThunderComment> replies = List.from(state.replies)..addAll(repliesResponse);
      List<ThunderComment> mentions = List.from(state.mentions)..addAll(mentionsResponse);
      List<ThunderPrivateMessage> privateMessages = List.from(state.privateMessages)..addAll(privateMessagesResponse);

      return emit(
        state.copyWith(
          status: InboxStatus.success,
          privateMessages: cleanDeletedMessages(privateMessages),
          mentions: cleanDeletedMentions(mentions),
          replies: replies,
          showUnreadOnly: state.showUnreadOnly,
          inboxMentionPage: state.inboxMentionPage + 1,
          inboxReplyPage: state.inboxReplyPage + 1,
          inboxPrivateMessagePage: state.inboxPrivateMessagePage + 1,
          hasReachedInboxReplyEnd: repliesResponse.isEmpty || repliesResponse.length < limit,
          hasReachedInboxMentionEnd: mentionsResponse.isEmpty == true || mentionsResponse.length < limit,
          hasReachedInboxPrivateMessageEnd: privateMessages.isEmpty == true || privateMessages.length < limit,
        ),
      );
    } catch (e) {
      emit(state.copyWith(
        status: InboxStatus.failure,
        errorMessage: e.toString(),
        totalUnreadCount: 0,
        repliesUnreadCount: 0,
        mentionsUnreadCount: 0,
        messagesUnreadCount: 0,
      ));
    }
  }

  /// Handles comment related actions on a given item within the inbox
  Future<void> _inboxItemActionEvent(InboxItemActionEvent event, Emitter<InboxState> emit) async {
    assert(!(event.commentReplyId == null && event.personMentionId == null && event.privateMessageId == null));
    emit(state.copyWith(status: InboxStatus.refreshing, errorMessage: ''));

    int existingIndex = -1;

    ThunderComment? existingCommentReplyView;
    ThunderComment? existingPersonMentionView;
    ThunderPrivateMessage? existingPrivateMessageView;

    if (event.commentReplyId != null && event.action == CommentAction.read) {
      existingIndex = state.replies.indexWhere((element) => element.replyMentionId == event.commentReplyId);
      existingCommentReplyView = state.replies[existingIndex];
    } else if (event.commentReplyId != null && event.action != CommentAction.read) {
      existingIndex = state.replies.indexWhere((element) => element.id == event.commentReplyId);
      existingCommentReplyView = state.replies[existingIndex];
    }

    if (event.personMentionId != null && event.action == CommentAction.read) {
      existingIndex = state.mentions.indexWhere((element) => element.replyMentionId == event.personMentionId);
      existingPersonMentionView = state.mentions[existingIndex];
    } else if (event.personMentionId != null && event.action != CommentAction.read) {
      existingIndex = state.mentions.indexWhere((element) => element.id == event.personMentionId);
      existingPersonMentionView = state.mentions[existingIndex];
    }

    if (event.privateMessageId != null) {
      existingIndex = state.privateMessages.indexWhere((element) => element.id == event.privateMessageId);
      existingPrivateMessageView = state.privateMessages[existingIndex];
    }

    if (existingCommentReplyView == null && existingPersonMentionView == null && existingPrivateMessageView == null) return emit(state.copyWith(status: InboxStatus.failure));

    /// Convert the reply or mention to a comment
    ThunderComment? comment;

    if (existingCommentReplyView != null) {
      comment = existingCommentReplyView;
    } else if (existingPersonMentionView != null) {
      comment = existingPersonMentionView;
    }

    switch (event.action) {
      case CommentAction.read:
        try {
          // Optimistically remove the reply from the list or change the status (depending on whether we're showing all)
          if (existingCommentReplyView != null) {
            if (!state.showUnreadOnly) {
              state.replies[existingIndex] = existingCommentReplyView.copyWith(read: event.value);
            } else if (event.value == true) {
              state.replies.remove(existingCommentReplyView);
            }
          } else if (existingPersonMentionView != null) {
            if (!state.showUnreadOnly) {
              state.mentions[existingIndex] = existingPersonMentionView.copyWith(read: event.value);
            } else if (event.value == true) {
              state.mentions.remove(existingPersonMentionView);
            }
          } else if (existingPrivateMessageView != null) {
            if (!state.showUnreadOnly) {
              state.privateMessages[existingIndex] = existingPrivateMessageView.copyWith(read: event.value);
            } else if (event.value == true) {
              state.privateMessages.remove(existingPrivateMessageView);
            }
          }

          if (existingCommentReplyView != null) {
            await notificationRepository.markReplyAsRead(
              replyId: event.commentReplyId!,
              read: event.value,
            );
          } else if (existingPersonMentionView != null) {
            await notificationRepository.markMentionAsRead(
              mentionId: event.personMentionId!,
              read: event.value,
            );
          } else if (existingPrivateMessageView != null) {
            await notificationRepository.markMessageAsRead(
              messageId: event.privateMessageId!,
              read: event.value,
            );
          }

          final unread = await notificationRepository.unreadNotificationsCount();
          int totalUnreadCount = unread['private_messages'] + unread['mentions'] + unread['replies'];

          return emit(state.copyWith(
            status: InboxStatus.success,
            totalUnreadCount: totalUnreadCount,
            repliesUnreadCount: unread['replies'],
            mentionsUnreadCount: unread['mentions'],
            messagesUnreadCount: unread['private_messages'],
            inboxReplyMarkedAsRead: event.commentReplyId,
          ));
        } catch (e) {
          return emit(state.copyWith(status: InboxStatus.failure, errorMessage: e.toString()));
        }
      case CommentAction.vote:
        try {
          ThunderComment updatedComment = optimisticallyVoteComment(comment!, event.value);

          if (existingCommentReplyView != null) {
            state.replies[existingIndex] = existingCommentReplyView.copyWith(
              score: updatedComment.score,
              upvotes: updatedComment.upvotes,
              downvotes: updatedComment.downvotes,
              myVote: updatedComment.myVote,
            );
          } else if (existingPersonMentionView != null) {
            state.mentions[existingIndex] = existingPersonMentionView.copyWith(
              score: updatedComment.score,
              upvotes: updatedComment.upvotes,
              downvotes: updatedComment.downvotes,
              myVote: updatedComment.myVote,
            );
          }

          // Immediately set the status, and continue
          emit(state.copyWith(status: InboxStatus.success));
          emit(state.copyWith(status: InboxStatus.refreshing));

          await commentRepository.vote(comment, event.value).timeout(timeout, onTimeout: () {
            // Restore the original comment if vote fails
            if (existingCommentReplyView != null) {
              state.replies[existingIndex] = existingCommentReplyView;
            } else if (existingPersonMentionView != null) {
              state.mentions[existingIndex] = existingPersonMentionView;
            }

            throw Exception(AppLocalizations.of(GlobalContext.context)!.timeoutUpvoteComment);
          });

          return emit(state.copyWith(status: InboxStatus.success));
        } catch (e) {
          return emit(state.copyWith(status: InboxStatus.failure, errorMessage: e.toString()));
        }
      case CommentAction.save:
        try {
          ThunderComment updatedComment = optimisticallySaveComment(comment!, event.value);

          if (existingCommentReplyView != null) {
            state.replies[existingIndex] = existingCommentReplyView.copyWith(saved: updatedComment.saved!);
          } else if (existingPersonMentionView != null) {
            state.mentions[existingIndex] = existingPersonMentionView.copyWith(saved: updatedComment.saved!);
          }

          // Immediately set the status, and continue
          emit(state.copyWith(status: InboxStatus.success));
          emit(state.copyWith(status: InboxStatus.refreshing));

          await commentRepository.save(comment, event.value).timeout(timeout, onTimeout: () {
            // Restore the original comment if saving fails
            if (existingCommentReplyView != null) {
              state.replies[existingIndex] = existingCommentReplyView;
            } else if (existingPersonMentionView != null) {
              state.mentions[existingIndex] = existingPersonMentionView;
            }

            throw Exception(AppLocalizations.of(GlobalContext.context)!.timeoutSaveComment);
          });

          return emit(state.copyWith(status: InboxStatus.success));
        } catch (e) {
          return emit(state.copyWith(status: InboxStatus.failure, errorMessage: e.toString()));
        }
      case CommentAction.delete:
        try {
          ThunderComment updatedComment = optimisticallyDeleteComment(comment!, event.value);

          if (existingCommentReplyView != null) {
            state.replies[existingIndex] = existingCommentReplyView.copyWith(
              deleted: updatedComment.deleted,
            );
          } else if (existingPersonMentionView != null) {
            state.mentions[existingIndex] = existingPersonMentionView.copyWith(
              deleted: updatedComment.deleted,
            );
          }

          // Immediately set the status, and continue
          emit(state.copyWith(status: InboxStatus.success));
          emit(state.copyWith(status: InboxStatus.refreshing));

          await commentRepository.delete(comment, event.value).timeout(timeout, onTimeout: () {
            // Restore the original comment if deleting fails
            if (existingCommentReplyView != null) {
              state.replies[existingIndex] = existingCommentReplyView;
            } else if (existingPersonMentionView != null) {
              state.mentions[existingIndex] = existingPersonMentionView;
            }

            throw Exception(AppLocalizations.of(GlobalContext.context)!.timeoutErrorMessage);
          });

          return emit(state.copyWith(status: InboxStatus.success));
        } catch (e) {
          return emit(state.copyWith(status: InboxStatus.failure, errorMessage: e.toString()));
        }
      default:
        return emit(state.copyWith(status: InboxStatus.failure, errorMessage: AppLocalizations.of(GlobalContext.context)!.unexpectedError));
    }
  }

  Future<void> _markAllAsRead(MarkAllAsReadEvent event, emit) async {
    try {
      emit(state.copyWith(status: InboxStatus.refreshing, errorMessage: ''));
      await notificationRepository.markAllNotificationsAsRead();

      // Update all the replies, mentions, and messages to be read locally
      List<ThunderComment> updatedReplies = state.replies.map((comment) => comment.copyWith(read: true)).toList();
      List<ThunderComment> updatedMentions = state.mentions.map((comment) => comment.copyWith(read: true)).toList();
      List<ThunderPrivateMessage> updatedPrivateMessages = state.privateMessages.map((privateMessage) => privateMessage.copyWith(read: true)).toList();

      return emit(state.copyWith(
        status: InboxStatus.success,
        replies: state.showUnreadOnly ? [] : updatedReplies,
        mentions: state.showUnreadOnly ? [] : updatedMentions,
        privateMessages: state.showUnreadOnly ? [] : updatedPrivateMessages,
        totalUnreadCount: 0,
        repliesUnreadCount: 0,
        mentionsUnreadCount: 0,
        messagesUnreadCount: 0,
      ));
    } catch (e) {
      emit(state.copyWith(status: InboxStatus.failure, errorMessage: e.toString()));
    }
  }

  List<ThunderPrivateMessage> cleanDeletedMessages(List<ThunderPrivateMessage> messages) {
    List<ThunderPrivateMessage> cleanMessages = [];

    for (ThunderPrivateMessage message in messages) {
      cleanMessages.add(cleanDeletedPrivateMessage(message));
    }

    return cleanMessages;
  }

  List<ThunderComment> cleanDeletedMentions(List<ThunderComment> mentions) {
    List<ThunderComment> cleanedMentions = [];

    for (ThunderComment mention in mentions) {
      cleanedMentions.add(cleanDeletedMention(mention));
    }

    return cleanedMentions;
  }

  ThunderPrivateMessage cleanDeletedPrivateMessage(ThunderPrivateMessage message) {
    if (message.deleted) {
      return message.copyWith(
        content: "_deleted by creator_",
      );
    }

    return message;
  }

  ThunderComment cleanDeletedMention(ThunderComment mention) {
    if (mention.removed) return mention.copyWith(content: "_deleted by moderator_");
    if (mention.deleted) return mention.copyWith(content: "_deleted by creator_");
    return mention;
  }
}
